package com.radar.io.cinrad;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.log.Log;
import cn.hutool.log.LogFactory;
import com.radar.algorithm.Cacl;
import com.radar.algorithm.DenseLucaskanade;
import com.radar.algorithm.MathCommon;
import com.radar.algorithm.SemiLagrangian;
import com.radar.common.*;
import com.radar.exception.RadarCaclError;
import com.radar.exception.RadarDecodeError;
import com.radar.io.RadarGridData;
import com.radar.io.VCSData;
import com.radar.utils.RefObj;
import org.bytedeco.javacpp.indexer.Indexer;
import org.bytedeco.opencv.global.opencv_imgcodecs;
import org.bytedeco.opencv.global.opencv_imgproc;
import org.bytedeco.opencv.opencv_core.Mat;
import org.meteoinfo.global.PointD;
import ucar.ma2.*;
import ucar.nc2.*;
import ucar.nc2.Dimension;
import visad.bom.RadarFileException;

import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.List;
import java.util.stream.IntStream;

/**
 * @author: fy-wbj
 * @description: cinrad radar struct
 * @date: 2020/9/18
 */
public abstract class BaseCinrad {

    static Log log = LogFactory.get();

    public BaseCinrad(String filePath, CinradType type, String code) {
        this.filePath = filePath;
        this.type = type;
        this.code = code;
        globalAttr = new ArrayList<>();
    }

    public BaseCinrad() {
        globalAttr = new ArrayList<>();
    }

    /**
     * file path
     */
    public String filePath;

    /**
     * cinrad type
     */
    public CinradType type;

    /**
     * site code
     */
    public String code;

    /**
     * netcdf tmp file write
     */
    public NetcdfFileWriter write;

    /**
     * netcdf tmp file
     */
    public NetcdfFile netcdf;

    /**
     * radar task
     */
    public int vcp = 21;

    public enum ScanType {
        PPI, VOL, RHI
    }

    /**
     * 编码值中的特殊标记,表示无有效观测数据
     */
    static int CODE_INVALID = 0;

    /**
     * 编码值中的特殊标记,表示有距离模糊
     */
    static int CODE_RANFOLD = 1;

    /**
     * 实际值中的特殊标记,表示有距离模糊
     */
    static float VALUE_RANFOLD = (float) -999.0;

    /**
     * 速度精度类型,代表的精度为0.5 M/S
     */
    static int RES_POINT_FIVE = 2;

    /**
     * 速度精度类型,代表的精度为1.0 M/S
     */
    static int RES_ONE_POINT = 4;

    /**
     * 体扫开始状态标志
     */
    static int VOL_BEG = 3;

    /**
     * 体扫结束状态标志
     */
    static int VOL_END = 4;

    /**
     * 仰角开始状态标志
     */
    static int ELV_BEG = 0;

    /**
     * 仰角结束状态标志
     */
    static int ELV_END = 2;

    /**
     * netcdf global Attribute
     */
    public List<Attribute> globalAttr;

    /**
     * scan time
     */
    public Date dateTime;

    public ScanType scanType = ScanType.VOL;

    private double[][] sortAzi;

    private String ncFile;

    private double projectStep = 0.01;

    public static NetcdfFileWriter.Version version = NetcdfFileWriter.Version.netcdf4;
    //endregion

    //region internal method

    double decodeRef(int code) {
        if (code == CODE_INVALID) {
            return GisConstants.UNDEF;
        } else if (code == CODE_RANFOLD) {
            return VALUE_RANFOLD;
        } else {
            return ((code - 2.0) / 2.0 - 32.5);
        }
    }

    double decodeVel(int code, int resType) {
        if (code == CODE_INVALID) {
            return GisConstants.UNDEF;
        } else if (code == CODE_RANFOLD) {
            return VALUE_RANFOLD;
        } else {
            //0.5 m/s
            if (resType == RES_POINT_FIVE) {
                return ((code - 2.0) / 2.0 - 63.5);
            } else {
                return ((code - 2) - 127.0);
            }
        }

    }

    double decodeSpw(int code) {
        if (code == CODE_INVALID) {
            return GisConstants.UNDEF;
        } else if (code == CODE_RANFOLD) {
            return VALUE_RANFOLD;
        } else {
            return ((code - 2.0) / 2.0 - 63.5);
        }
    }

    double decodeRf(int code) {
        if (code == CODE_INVALID) {
            return GisConstants.UNDEF;
        } else if (code == CODE_RANFOLD) {
            return VALUE_RANFOLD;
        } else {
            return code;
        }
    }

    /**
     * init global attribute
     *
     * @throws RadarDecodeError
     */
    protected void setGlobalAttr() throws RadarDecodeError {
        Attribute title = new Attribute("title", "Basic cinrad radar Struct");
        this.globalAttr.add(title);
        if (this.filePath != null) {
            Attribute fileName = new Attribute("fileName", new File(filePath).getName());
            this.globalAttr.add(fileName);
        }
        Attribute references = new Attribute("references", "https://github.com/CyanideCN/PyCINRAD");
        this.globalAttr.add(references);
        Attribute description = new Attribute("description", String.format("radar vcp is : %s Observation time : %s , cinrad type : %s",
                this.vcp,
                DateUtil.format(this.dateTime, DateConstants.DATE_FORMAT_YYYYMMDDHHMM),
                this.type.name()));
        this.globalAttr.add(description);
        this.globalAttr.add(new Attribute("task", "VCP" + vcp));
        this.globalAttr.add(new Attribute("scan_time", DateUtil.format(this.dateTime, DateConstants.DATE_TIME_FORMAT_YYYY_MM_DD_HH_MI)));
        try {
            RadarSite.RadarSiteItem radarSiteItem = getRadarSite();
            if (radarSiteItem != null) {
                this.globalAttr.add(new Attribute("site_code", radarSiteItem.getStation()));
                this.globalAttr.add(new Attribute("site_name", radarSiteItem.getStationName()));
                this.globalAttr.add(new Attribute("site_province", radarSiteItem.getProvince()));
                this.globalAttr.add(new Attribute("site_city", radarSiteItem.getCity()));
                this.globalAttr.add(new Attribute("site_longitude", radarSiteItem.getLon()));
                this.globalAttr.add(new Attribute("site_latitude", radarSiteItem.getLat()));
                this.globalAttr.add(new Attribute("site_altitude", radarSiteItem.getAlt()));
            }
        } catch (RadarDecodeError e) {
            log.error(e.getMessage());
        }
        globalAttr.forEach(i -> this.netcdf.addAttribute(this.netcdf.getRootGroup(), i));
    }

    /**
     * The radar class library uses the netcdf data format to temporarily create a file + .nc temporary file
     */
    protected void initNetcdfTemp() throws RadarFileException, RadarDecodeError {
        if (ncFile == null) {
            ncFile = this.filePath + ".nc";
        }
        String ncTemp = ncFile;
        // netcdf4 需要安装netcdf 类库 这里使用netcdf3 并且加入分片策略
//        Nc4ChunkingDefault nc4ChunkingDefault = new Nc4ChunkingDefault();
        try {
            if (FileUtil.exist(ncTemp)) {
                write = NetcdfFileWriter.openExisting(ncTemp);
                this.netcdf = write.getNetcdfFile();
            } else {
                try {
                    write = NetcdfFileWriter.createNew(version, ncTemp);
                }catch (Exception e){
                    version = NetcdfFileWriter.Version.netcdf3;
                    write = NetcdfFileWriter.createNew(version, ncTemp);
                }

            }
        } catch (IOException e) {
            try {
                FileUtil.del(ncTemp);
                try {
                    write = NetcdfFileWriter.createNew(version, ncTemp);
                }catch (Exception e1){
                    version = NetcdfFileWriter.Version.netcdf3;
                    write = NetcdfFileWriter.createNew(version, ncTemp);
                }
            } catch (IOException ioException) {
                throw new RadarFileException("radar temp netcdf file throw IOException");
            }
        }
        double[] elevation = getElevation();
        double[][] azimuth = getAzimuth();
        Variable v1;
        Variable v2;
        Dimension dimension;
        Dimension dimension1;
        if (this.netcdf == null || this.netcdf.findDimension("elevation") == null) {
            dimension = write.addDimension(null, "elevation", elevation.length);
            v1 = write.addVariable(null, "elevation", DataType.DOUBLE, "elevation");
            v1.addAttribute(new Attribute("units", "rad"));
        } else {
            dimension = netcdf.findDimension("elevation");
            v1 = netcdf.findVariable("elevation");
        }
        if (this.netcdf == null || this.netcdf.findDimension("azimuth") == null) {
            dimension1 = write.addDimension(null, "azimuth", azimuth[0].length);
            v2 = write.addVariable(null, "azimuth", DataType.DOUBLE, Arrays.asList(dimension, dimension1));
            v2.addAttribute(new Attribute("units", "rad"));
        } else {
            v2 = netcdf.findVariable("azimuth");
        }
        try {
            if (this.write.isDefineMode()) {
                write.create();
                this.netcdf = write.getNetcdfFile();
            }
            ArrayDouble elevalues = new ArrayDouble.D1(elevation.length);
            ArrayDouble azivalues = new ArrayDouble.D2(azimuth.length, azimuth[0].length);
            IntStream.range(0, elevation.length).forEach(i -> elevalues.setDouble(i, elevation[i]));
            int index = 0;
            for (int i = 0; i < azimuth.length; i++) {
                for (int j = 0; j < azimuth[0].length; j++) {
                    azivalues.setDouble(index, azimuth[i][j]);
                    index++;
                }
            }
            v1.setCachedData(elevalues);
            v2.setCachedData(azivalues);
        } catch (Exception e) {
            e.printStackTrace();
            throw new RadarFileException("Cannot write radar temp file");
        }
        this.setGlobalAttr();
    }

    protected void setLonLat(RadarGridData gridData, ProductType type) throws IOException, InvalidRangeException {
        write.addDimension(null, "Longitude_" + type.name(), gridData.getXNum());
        write.addDimension(null, "Latitude_" + type.name(), gridData.getYNum());

        Variable v1 = write.addVariable(null, "Longitude_" + type.name(), DataType.DOUBLE, "Longitude_" + type.name());
        v1.addAttribute(new Attribute("units", "degrees_east"));
        v1.addAttribute(new Attribute("actual_range", Arrays.asList(gridData.getXMin(), gridData.getXMax())));

        Variable v2 = write.addVariable(null, "Latitude_" + type.name(), DataType.DOUBLE, "Latitude_" + type.name());
        v2.addAttribute(new Attribute("units", "degrees_north"));
        v2.addAttribute(new Attribute("actual_range", Arrays.asList(gridData.getYMin(), gridData.getYMax())));

        ArrayDouble lonArray = MathCommon.toArray(gridData.xArray);
        ArrayDouble latArray = MathCommon.toArray(gridData.yArray);

        v1.setCachedData(lonArray);
        v2.setCachedData(latArray);
    }

    /**
     * return elevation
     * Exclude invalid data
     *
     * @param type
     * @return
     */
    private double[] getElevation(ProductType type) {
        int[] elevationIndex = this.getElevationIndex(type);
        double[] ele = new double[elevationIndex.length];
        double[] elevation = this.getElevation();
        IntStream.range(0, ele.length).forEach(i -> ele[i] = elevation[elevationIndex[i]]);
        return ele;
    }


    //endregion

    // region abstract

    /**
     * abstract parse cinrad file
     *
     * @param bytes
     * @throws RadarDecodeError
     */
    public abstract void parse(byte[] bytes) throws RadarDecodeError;

    /**
     * abstract getElevation
     *
     * @return
     */
    public abstract double[] getElevation();

    /**
     * abstract get real index Elevation
     *
     * @return
     */
    public abstract int[] getElevationIndex(ProductType type);

    /**
     * abstract getAzimuth
     *
     * @return
     */
    public abstract double[][] getAzimuth();

    /**
     * get resoultion units m
     *
     * @param type
     * @return
     */
    public abstract double getResolution(ProductType type);

    // endregion

    //region interface

    public void setNetcdfFile(String file) {
        if(file != null){
            this.ncFile = file;
        }
    }

    public void setProjectStep(double projectStep) {
        this.projectStep = projectStep;
    }

    /**
     * return simple polar radar data
     *
     * @param layers
     * @param type
     */
    public byte[][] getRaw(int layers, ProductType type) throws RadarCaclError {
        Variable variable = this.netcdf.findVariable(type.name());
        if (variable == null) {
            throw new RadarCaclError("not found variable type : " + type.name());
        }
        List<Dimension> dimensions = variable.getDimensions();
        if (layers > dimensions.get(0).getLength() - 1) {
            throw new RadarCaclError("The current elevation layer data has exceeded Dimension length layers: " + layers);
        }
        try {
            Array read = variable.read(new int[]{layers, 0, 0}, new int[]{1, dimensions.get(1).getLength(), dimensions.get(2).getLength()});
            // find resu
            return (byte[][]) read.reduce().copyToNDJavaArray();
        } catch (Exception e) {
            throw new RadarCaclError(e.getMessage());
        }
    }

    /**
     * return all raw data
     * Exclude invalid data
     *
     * @param type
     */
    public byte[][][] getRaw(ProductType type) throws RadarCaclError {
        Variable variable = netcdf.findVariable(type.name());
        if (variable == null) {
            throw new RadarCaclError("not found variable type : " + type.name());
        }
        List<Dimension> dimensions = variable.getDimensions();
        try {
            Array read = variable.read(new int[]{0, 0, 0}, new int[]{dimensions.get(0).getLength(), dimensions.get(1).getLength(), dimensions.get(2).getLength()});
            byte[][][] dat = (byte[][][]) read.reduce().copyToNDJavaArray();
            return dat;
        } catch (Exception e) {
            throw new RadarCaclError(e.getMessage());
        }
    }

    public RadarSite.RadarSiteItem getRadarSite() throws RadarDecodeError {
        Optional<RadarSite.RadarSiteItem> item = RadarSite.getInstance().getItem(this.code);
        if (!item.isPresent()) {
            throw new RadarDecodeError("not found radar site setting");
        }
        RadarSite.RadarSiteItem radarSiteItem = item.get();
        return radarSiteItem;
    }

    /**
     * return all ppi
     *
     * @param type element type
     * @return
     * @throws RadarCaclError
     * @throws RadarDecodeError
     */
    public List<RadarGridData> allPPI(ProductType type) throws IOException, RadarCaclError, InvalidRangeException, RadarDecodeError {
        double[] ele = getElevation();
        int[] layers = new int[ele.length];
        IntStream.range(0, layers.length).forEach(i -> layers[i] = i);
        return getPPI(layers, type);
    }

    /**
     * return Single layer ppi project lonlat
     *
     * @param layers layer index
     * @param type   element type
     * @return
     * @throws RadarCaclError
     * @throws RadarDecodeError
     */
    public RadarGridData getPPI(int layers, ProductType type) throws RadarCaclError, RadarDecodeError, IOException, InvalidRangeException {
        return getPPI(new int[]{layers}, type).get(0);
    }

    /**
     * return Single layers ppi project lonlat
     *
     * @param layers layer index []
     * @param type   element type
     * @return
     * @throws RadarCaclError
     * @throws RadarDecodeError
     */
    public List<RadarGridData> getPPI(int[] layers, ProductType type) throws RadarCaclError, RadarDecodeError, IOException, InvalidRangeException {
        List<RadarGridData> list = new ArrayList<>();
        Variable variable = this.netcdf.findVariable("PPI_" + type.name());
        Attribute layer_index;
        int[] layer_indexs = new int[]{-1};
        if (variable != null) {
            layer_index = variable.findAttribute("layer_index");
            layer_indexs = (int[]) layer_index.getValues().copyToNDJavaArray();
        }
        double resoultion = getResolution(type) / 1000.0;
        double[][] azimuth = getAzimuth();
        double[] ele = getElevation();
        Array ppiArray = null;
        for (int i = 0; i < layers.length; i++) {
            int index = -1;
            if (variable != null) {
                index = ArrayUtil.indexOf(layer_indexs, layers[i]);
            }
            RadarGridData gridData = new RadarGridData();
            if (index != -1) {
                gridData.xArray = (double[]) this.netcdf.findVariable("Longitude_" + type.name()).read().copyToNDJavaArray();
                gridData.yArray = (double[]) this.netcdf.findVariable("Latitude_" + type.name()).read().copyToNDJavaArray();
                Array read = variable.read(new int[]{layers[i], 0, 0}, new int[]{1, gridData.yArray.length, gridData.xArray.length});
                // find resu
                byte[][] data = (byte[][]) read.reduce().copyToNDJavaArray();
                gridData.radarData = data;
                gridData.projInfo = GisConstants.lonlat;
                gridData.missingValue = GisConstants.UNDEF;
            } else {
                long time = System.currentTimeMillis();
                byte[][] refs = getRaw(layers[i], type);
//                log.info("get raw data type:{} layers:{}, ele:{}  time: {}", type.name(), layers[i], ele[layers[i]], System.currentTimeMillis() - time);
                byte[][] gridppi = Cacl.gridppi(azimuth[layers[i]], refs, type);
//                log.info("caci gridppi type:{} layers:{}, ele:{}  time: {}", type.name(), layers[i], ele[layers[i]], System.currentTimeMillis() - time);
                gridData = Cacl.project(gridppi, ele[layers[i]],
                        getRadarSite().getLon(),
                        getRadarSite().getLat(),
                        resoultion,projectStep);
                double[] minmax = gridData.getMaxMinValue(type);
//                log.info("get ppi type:{} layers:{}, ele:{} min-max dbz:{} - {} time: {}", type.name(), layers[i], ele[layers[i]], minmax[1], minmax[0], System.currentTimeMillis() - time);
            }
            if (i == 0) {
                // init Longitude
                if (this.netcdf.findDimension("Longitude_" + type.name()) == null) {
                    setLonLat(gridData, type);
                }
                // init ppiArray
                if (variable == null) {
                    variable = write.addVariable(null, "PPI_" + type.name(), DataType.BYTE, Arrays.asList(
                            netcdf.findDimension("elevation"),
                            netcdf.findDimension("Latitude_" + type.name()),
                            netcdf.findDimension("Longitude_" + type.name())));
                    ppiArray = new ArrayByte.D3(this.getElevation().length, gridData.getYNum(), gridData.getXNum());
                    MathCommon.fillByte(ppiArray);

                    variable.addAttribute(new Attribute("missing_value", (byte) -128));
                    variable.addAttribute(new Attribute("add_offset", 32.5f));
                    variable.addAttribute(new Attribute("scale_factor", 0.5f));
                    variable.addAttribute(new Attribute("full_name", ProductType.REF.getFullName()));
                    variable.addAttribute(new Attribute("units", ProductType.REF.getUnit()));

                } else {
                    ppiArray = variable.read();
                }
            }
            // set Array ppi D3
            int x = layers[i] * gridData.getYNum() * gridData.getXNum();
            for (int j = 0; j < gridData.getYNum(); j++) {
                for (int k = 0; k < gridData.getXNum(); k++) {
                    ppiArray.setByte(x, gridData.radarData[j][k]);
                    x++;
                }
            }
            list.add(gridData);
        }
        Array indexArray;
        int index = 0;
        Attribute attribute = variable.findAttribute("layer_index");
        if (attribute != null) {
            Array oldIndexArray = attribute.getValues();
            indexArray = new ArrayInt.D1((int) (layers.length + oldIndexArray.getSize()));
            for (int i = 0; i < oldIndexArray.getSize(); i++) {
                indexArray.setDouble(i, oldIndexArray.getInt(i));
                index++;
            }
            variable.remove(attribute);
        } else {
            indexArray = new ArrayInt.D1(layers.length);
        }
        for (int i = 0; i < layers.length; i++) {
            indexArray.setDouble(index, layers[i]);
            index++;
        }
        variable.addAttribute(new Attribute("layer_index", indexArray));
        variable.setCachedData(ppiArray);

        return list;
    }

    public RadarGridData getCR() throws IOException, RadarCaclError, InvalidRangeException, RadarDecodeError {
        Variable variable = netcdf.findVariable("CR");
        if (variable != null) {
            RadarGridData gridData = new RadarGridData();
            gridData.projInfo = GisConstants.lonlat;
            gridData.missingValue = GisConstants.UNDEF;
            gridData.xArray = (double[]) this.netcdf.findVariable("Longitude_" + ProductType.REF.name()).read().copyToNDJavaArray();
            gridData.yArray = (double[]) this.netcdf.findVariable("Latitude_" + ProductType.REF.name()).read().copyToNDJavaArray();
            Array read = variable.read(new int[]{0, 0}, new int[]{gridData.yArray.length, gridData.xArray.length});
            gridData.radarData = (byte[][]) read.reduce().copyToNDJavaArray();
            return gridData;
        }
        double[] ele = this.getElevation();
        RadarGridData cr = new RadarGridData();
        byte[][][] z = new byte[0][0][0];
        List<RadarGridData> list = this.allPPI(ProductType.REF);
        for (int i = 0; i < list.size(); i++) {
            RadarGridData ppi = list.get(i);
            if (i == 0) {
                z = new byte[ele.length][ppi.getYNum()][ppi.getXNum()];
                cr.xArray = ppi.xArray;
                cr.yArray = ppi.yArray;
                cr.missingValue = ppi.missingValue;
                cr.projInfo = ppi.projInfo;
            }
            z[i] = ppi.radarData.clone();
        }
        cr.radarData = Cacl.cr(z);
        variable = write.addVariable(null, "CR", DataType.BYTE, Arrays.asList(
                netcdf.findDimension("Latitude_" + ProductType.REF.name()),
                netcdf.findDimension("Longitude_" + ProductType.REF.name())));

        variable.addAttribute(new Attribute("missing_value", (byte) -128));
        variable.addAttribute(new Attribute("add_offset", 32.5));
        variable.addAttribute(new Attribute("scale_factor", 0.5f));
        variable.addAttribute(new Attribute("full_name", ProductType.CR.getFullName()));
        variable.addAttribute(new Attribute("units", ProductType.CR.getUnit()));

        variable.setCachedData(MathCommon.toByteArray(cr.radarData));
//        log.info("cr maxmin value : {} - {}", cr.getMaxMinValue(ProductType.REF)[0], cr.getMaxMinValue(ProductType.REF)[1]);
        return cr;
    }

    public byte[][] quickCR() throws IOException, RadarCaclError, InvalidRangeException, RadarDecodeError {
        byte[][][] raw = this.getRaw(ProductType.REF);
        double[][] azimuth = this.getAzimuth();
        double[] elevation = this.getElevation();
        double resolution = this.getResolution(ProductType.REF);
        byte[][] cr = Cacl.quickCR(raw, azimuth, elevation, resolution, 15000);
        return cr;
    }

    public RadarGridData getET() throws IOException, RadarCaclError, InvalidRangeException, RadarDecodeError {
        return this.getET(0);
    }

    public RadarGridData getET(double threshold) throws IOException, RadarCaclError, InvalidRangeException, RadarDecodeError {
        Variable variable = netcdf.findVariable("ET");
        if (variable != null) {
            RadarGridData gridData = new RadarGridData();
            gridData.projInfo = GisConstants.lonlat;
            gridData.missingValue = GisConstants.UNDEF;
            gridData.xArray = (double[]) this.netcdf.findVariable("Longitude_" + ProductType.REF.name()).read().copyToNDJavaArray();
            gridData.yArray = (double[]) this.netcdf.findVariable("Latitude_" + ProductType.REF.name()).read().copyToNDJavaArray();
            Array read = variable.read(new int[]{0, 0}, new int[]{gridData.yArray.length, gridData.xArray.length});
            gridData.caclData = (short[][]) read.reduce().copyToNDJavaArray();
            return gridData;
        }
        double[] elevation = this.getElevation();
        RadarGridData et = new RadarGridData();
        byte[][][] z = new byte[0][0][0];
        List<RadarGridData> list = this.allPPI(ProductType.REF);
        for (int i = 0; i < list.size(); i++) {
            RadarGridData ppi = list.get(i);
            if (i == 0) {
                z = new byte[elevation.length][ppi.getYNum()][ppi.getXNum()];
                et.xArray = ppi.xArray;
                et.yArray = ppi.yArray;
                et.missingValue = ppi.missingValue;
                et.projInfo = ppi.projInfo;
            }
            z[i] = ppi.radarData.clone();
        }
        et = Cacl.et(z, et, threshold, elevation, this.getRadarSite().getAlt(), this.getResolution(ProductType.REF) / 1000.0);
        variable = write.addVariable(null, "ET", DataType.SHORT, Arrays.asList(
                netcdf.findDimension("Latitude_" + ProductType.REF.name()),
                netcdf.findDimension("Longitude_" + ProductType.REF.name())));

        variable.addAttribute(new Attribute("missing_value", (short) -128));
        variable.addAttribute(new Attribute("add_offset", 0f));
        variable.addAttribute(new Attribute("scale_factor", 0.01f));
        variable.addAttribute(new Attribute("full_name", ProductType.ET.getFullName()));
        variable.addAttribute(new Attribute("units", ProductType.ET.getUnit()));

        variable.setCachedData(MathCommon.toShortArray(et.caclData));
        log.info("et maxmin value : {} - {}", et.getCaclMaxMinValue()[0], et.getCaclMaxMinValue()[1]);
        return et;
    }

    public RadarGridData getVIL() throws IOException, RadarCaclError, InvalidRangeException, RadarDecodeError {
        return this.getVIL(18);
    }

    public RadarGridData getVIL(double threshold) throws IOException, RadarCaclError, InvalidRangeException, RadarDecodeError {
        Variable variable = netcdf.findVariable("VIL");
        if (variable != null) {
            RadarGridData gridData = new RadarGridData();
            gridData.projInfo = GisConstants.lonlat;
            gridData.missingValue = GisConstants.UNDEF;
            gridData.xArray = (double[]) this.netcdf.findVariable("Longitude_" + ProductType.REF.name()).read().copyToNDJavaArray();
            gridData.yArray = (double[]) this.netcdf.findVariable("Latitude_" + ProductType.REF.name()).read().copyToNDJavaArray();
            Array read = variable.read(new int[]{0, 0}, new int[]{gridData.yArray.length, gridData.xArray.length});
            gridData.caclData = (short[][]) read.reduce().copyToNDJavaArray();
            return gridData;
        }
        double[] elevation = this.getElevation();
        RadarGridData vil = new RadarGridData();
        byte[][][] z = new byte[0][0][0];
        List<RadarGridData> list = this.allPPI(ProductType.REF);
        for (int i = 0; i < list.size(); i++) {
            RadarGridData ppi = list.get(i);
            if (i == 0) {
                z = new byte[elevation.length][ppi.getYNum()][ppi.getXNum()];
                vil.xArray = ppi.xArray;
                vil.yArray = ppi.yArray;
                vil.missingValue = ppi.missingValue;
                vil.projInfo = ppi.projInfo;
            }
            z[i] = ppi.radarData.clone();
        }
        vil = Cacl.vil(z, vil, threshold, elevation, this.getResolution(ProductType.REF) / 1000.0);
        variable = write.addVariable(null, "VIL", DataType.SHORT, Arrays.asList(
                netcdf.findDimension("Latitude_" + ProductType.REF.name()),
                netcdf.findDimension("Longitude_" + ProductType.REF.name())));

        variable.addAttribute(new Attribute("missing_value", (short) -128));
        variable.addAttribute(new Attribute("add_offset", 0f));
        variable.addAttribute(new Attribute("scale_factor", 0.01f));
        variable.addAttribute(new Attribute("full_name", ProductType.VIL.getFullName()));
        variable.addAttribute(new Attribute("units", ProductType.VIL.getUnit()));

        variable.setCachedData(MathCommon.toShortArray(vil.caclData));

        log.info("vil maxmin value : {} - {}", vil.getCaclMaxMinValue()[0], vil.getCaclMaxMinValue()[1]);
        return vil;
    }

    public double point(double x, double y, ProductType productType, int layers) throws RadarDecodeError, RadarCaclError {
        double elevation = this.getElevation()[layers];
        double resoultion = this.getResolution(productType) / 1000.0;
        double distince = MathCommon.getDistanceKM(x, this.getRadarSite().getLon(), y, this.getRadarSite().getLat());
        distince = distince / Math.cos(elevation * Const.DEG2RAD);
        distince /= resoultion;
        double angle = MathCommon.getAngleXY(y, x, this.getRadarSite().getLat(), this.getRadarSite().getLon());
        byte[][] raw = this.getRaw(layers, productType);
        RefObj<double[]> newazi = new RefObj<>(null);
        RefObj<byte[][]> newz = new RefObj<>(null);
        Cacl.sortAzi(this.getAzimuth()[layers], raw, newazi, newz);
        if (distince > newz.getValue()[0].length - 1) {
            return GisConstants.UNDEF;
        }
        return Cacl.interpolatePolar(angle, newazi.getValue(), distince, newz.getValue(), productType);
    }

    public VCSData vcs(List<PointD> points, int lonlatPoints, int altPoints, ProductType productType, double maxAlt) throws RadarCaclError, RadarDecodeError {
        List<PointD> repoint = MathCommon.getDisPoint(points, lonlatPoints);
        VCSData vcs = new VCSData();
        int[] index = getElevationIndex(productType);
        byte[][][] raw = this.getRaw(productType);
        double[] ele = this.getElevation(productType);
        double[][] azimuth = this.getAzimuth();
        List<double[]> azis = new ArrayList<>();
        List<byte[][]> raws = new ArrayList<>();
        for (int i = 0; i < index.length; i++) {
            RefObj<double[]> sortAzi = new RefObj<>(null);
            RefObj<byte[][]> sortZ = new RefObj<>(null);
            Cacl.sortAzi(azimuth[index[i]], raw[index[i]], sortAzi, sortZ);
            azis.add(sortAzi.getValue());
            raws.add(sortZ.getValue());
        }
        vcs.data = new double[altPoints][repoint.size()];
        for (int i = 0; i < vcs.data.length; i++) {
            double alt = maxAlt / altPoints * i;
            alt /= (this.getResolution(productType) / 1000.0);
            for (int j = 0; j < repoint.size(); j++) {
                double angle = MathCommon.getAngleXY(repoint.get(j).Y, repoint.get(j).X, this.getRadarSite().getLat(), this.getRadarSite().getLon());
                double distince = MathCommon.getDistanceKM(repoint.get(j).X, this.getRadarSite().getLon(), repoint.get(j).Y, this.getRadarSite().getLat());
                distince /= (this.getResolution(productType) / 1000.0);
                vcs.data[altPoints - 1 - i][j] = Cacl.bilinearInterpolat(angle, azis, ele, alt, distince, raws, productType);
            }
        }
        vcs.setLonLat(repoint);
        return vcs;
    }

    public byte[][][] cappi(ProductType productType, double[] alts) throws RadarCaclError {
        int[] index = getElevationIndex(productType);
        byte[][][] raw = this.getRaw(productType);
        double[] ele = this.getElevation(productType);
        double[][] azimuth = this.getAzimuth();
        List<double[]> azis = new ArrayList<>();
        List<byte[][]> raws = new ArrayList<>();
        for (int i = 0; i < index.length; i++) {
            RefObj<double[]> sortAzi = new RefObj<>(null);
            RefObj<byte[][]> sortZ = new RefObj<>(null);
            Cacl.sortAzi(azimuth[index[i]], raw[index[i]], sortAzi, sortZ);
            azis.add(sortAzi.getValue());
            raws.add(sortZ.getValue());
        }
        byte[][][] datas = new byte[alts.length][0][0];
        double resolution = this.getResolution(productType);
        IntStream.range(0, alts.length).parallel().forEach(i -> {
            long time = System.currentTimeMillis();
            datas[i] = Cacl.heightInterpolation(azis, ele, alts[i], raws, resolution, productType);
//            log.info("cappi end alts: {},time :{}", alts[i], System.currentTimeMillis() - time);
        });
        return datas;
    }

    public byte[][][] cappiTest(ProductType productType, double[] alts, double[] xarray, double[] yarray) throws RadarCaclError, RadarDecodeError {
        int[] index = getElevationIndex(productType);
        byte[][][] raw = this.getRaw(productType);
        double[] ele = this.getElevation(productType);
        double[][] azimuth = this.getAzimuth();
        List<double[]> azis = new ArrayList<>();
        List<byte[][]> raws = new ArrayList<>();
        for (int i = 0; i < index.length; i++) {
            RefObj<double[]> sortAzi = new RefObj<>(null);
            RefObj<byte[][]> sortZ = new RefObj<>(null);
            Cacl.sortAzi(azimuth[index[i]], raw[index[i]], sortAzi, sortZ);
            azis.add(sortAzi.getValue());
            raws.add(sortZ.getValue());
        }
        byte[][][] datas = new byte[alts.length][0][0];
        double resolution = this.getResolution(productType);
        RadarSite.RadarSiteItem radarSiteItem = this.getRadarSite();
        IntStream.range(0, alts.length).parallel().forEach(i -> {
            long time = System.currentTimeMillis();
            datas[i] = Cacl.heightInterpolation(azis, ele, alts[i], raws, resolution, productType, xarray, yarray, radarSiteItem.getLat(), radarSiteItem.getLon());
//            log.info("cappi end alts: {},time :{}", alts[i], System.currentTimeMillis() - time);
        });
        return datas;
    }

    public float[][][] semiLagrangian(String preFile, String nextFile, double offset, double forecastOffset, int num) throws InvalidRangeException, RadarDecodeError, RadarCaclError, IOException {
        log.info("start forcast f1:{}, f2:{}", preFile, nextFile);
        Mat ff1 = opencv_imgcodecs.imread(preFile);
        Mat ff2 = opencv_imgcodecs.imread(nextFile);
        Mat prvs = ff1.clone();
        Mat next = ff1.clone();
        opencv_imgproc.cvtColor(ff1, prvs, opencv_imgproc.COLOR_BGR2GRAY);
        opencv_imgproc.cvtColor(ff2, next, opencv_imgproc.COLOR_BGR2GRAY);

        int w = prvs.size().get(0);
        int h = prvs.size().get(1);
        double[][] pre = new double[h][w];
        double[][] nex = new double[h][w];
        Indexer indexer = prvs.createIndexer();
        Indexer indexer1 = next.createIndexer();
        for (int i = 0; i < h; i++) {
            for (int j = 0; j < w; j++) {
                pre[i][j] = indexer.getDouble(i, j);
                if (pre[i][j] == 255) {
                    pre[i][j] = 0;
                }
                nex[i][j] = indexer1.getDouble(i, j);
                if (nex[i][j] == 255) {
                    nex[i][j] = 0;
                }
            }
        }
        double[][][] cacl = DenseLucaskanade.cacl(new double[][][]{pre, nex});
        log.info("end flow  f1:{}, f2:{}", preFile, nextFile);
        double[][] u = cacl[0];
        double[][] v = cacl[1];
        for (int i = 0; i < h; i++) {
            for (int j = 0; j < w; j++) {
                u[i][j] = u[i][j] * (forecastOffset / offset);
                v[i][j] = v[i][j] * (forecastOffset / offset);
            }
        }
        RadarGridData cr = this.getCR();
        double[][] decode = MathCommon.decode(cr.radarData, ProductType.REF);
        SemiLagrangian semiLagrangian = new SemiLagrangian();
        int t = num;
        float[][][] extrapolate = semiLagrangian.extrapolate(MathCommon.doubleToFloat(decode), MathCommon.doubleToFloat(u), MathCommon.doubleToFloat(v), t);
        log.info("end SemiLagrangian f1:{}, f2:{}", preFile, nextFile);
        for (int i = 0; i < t; i++) {
            for (int j = 0; j < u.length; j++) {
                for (int k = 0; k < u[0].length; k++) {
                    extrapolate[i][j][k] = extrapolate[i][j][k] == 0 || extrapolate[i][j][k] == Float.NaN ? GisConstants.UNDEF : extrapolate[i][j][k];
                }
            }
        }
        return extrapolate;
    }

    /**
     * 只能存一次
     *
     * @param data
     */
    public void writeSemiLagrangian(float[][][] data, double forecastOffset, int num) {
        write.addDimension(null, "forecastDates", num);
        Variable v1 = write.addVariable(null, "forecastDates", DataType.DOUBLE, "forecastDates");
        v1.addAttribute(new Attribute("units", "minute"));
        ArrayDouble minutes = new ArrayDouble.D1(num);
        for (int i = 0; i < num; i++) {
            minutes.setDouble(i, num * forecastOffset);
        }
        v1.setCachedData(minutes);


        Variable variable = write.addVariable(null, "CR_FORECAST", DataType.FLOAT, Arrays.asList(
                netcdf.findDimension("forecastDates"),
                netcdf.findDimension("Latitude_" + ProductType.REF.name()),
                netcdf.findDimension("Longitude_" + ProductType.REF.name())));


        variable.addAttribute(new Attribute("missing_value", (float) GisConstants.UNDEF));
        variable.addAttribute(new Attribute("full_name", ProductType.CR.getFullName() + " forecast"));
        variable.addAttribute(new Attribute("units", ProductType.CR.getUnit()));

        variable.setCachedData(MathCommon.toFloatArray(data));
    }

    public void dispose() {
        this.close();
        File f = new File(this.ncFile);
        if (f.exists()) {
            f.delete();
        }
    }

    public void close() {
        try {
            if (this.write != null) {
//                this.write.abort();
                this.write.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            if (this.netcdf != null) {
                this.netcdf.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        this.netcdf = null;
        this.write = null;
    }

    public void flush() throws IOException, InvalidRangeException {
        if (this.write.isDefineMode()) {
            this.write.setRedefineMode(false);
            for (Variable variable : this.netcdf.getVariables()) {
                this.write.write(variable, variable.read());
            }
        }
        this.close();
    }

    public File getNetCDF() {
        return new File(this.ncFile);
    }

    //endregion

}
